
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.channels.spi.SelectorProvider;
import java.util.*;
import java.util.regex.*;

public class SIPTalk implements Runnable {
	
	final int NOT_REGISTERED = 0;
	final int REGISTERED = 1;
	final int CALL_SETUP = 2;
	final int CALL_IN_PROGRESS = 3;
	
	int uaState = NOT_REGISTERED;
	
    SIPTalkUI sipTalkUI;
    RspHandler rspHandler;
    Packet packet;
    Thread t;
    
	// The host:port combination to connect to
	private InetAddress serverAddress;
	private int serverPort;

	// The selector we'll be monitoring
	private Selector selector;

	// The buffer into which we'll read data when it's available
	private ByteBuffer readBuffer = ByteBuffer.allocate(8192);
	
	// A list of PendingChange instances
	private List pendingChanges = new LinkedList();

	// Maps a SocketChannel to a list of ByteBuffer instances
	private Map pendingData = new HashMap();
	
	// Maps a SocketChannel to a RspHandler
	private Map rspHandlers = Collections.synchronizedMap(new HashMap());
	
	private SocketChannel socket = null;
	private static SIPTalk instance = null;
	
	String bParty = null;
	
	RingPlayer ringPlayer = null;
	
	public SIPTalk(SIPTalkUI sipTalkUI, InetAddress serverAddress, int serverPort) throws IOException {
		this.sipTalkUI = sipTalkUI;
		this.serverAddress = serverAddress;
		this.serverPort = serverPort;
		this.selector = this.initSelector();
		packet = new Packet();
		rspHandler = new RspHandler(this, this.sipTalkUI);
		ringPlayer = RingPlayer.getInstance();
	}
	
	public static SIPTalk getInstance(SIPTalkUI sipTalkUI, InetAddress serverAddress, int serverPort) {
        try {
            if(instance == null) {
                instance = new SIPTalk(sipTalkUI, serverAddress, serverPort);
            }
        } catch(Exception e) { e.printStackTrace(); }
        return instance;
    }

	public void send(byte[] data, RspHandler handler) throws IOException {
		// Register the response handler
		this.rspHandlers.put(socket, handler);
		
		if (socket.isConnected())
			socket.keyFor(selector).interestOps(SelectionKey.OP_WRITE);
		
		// And queue the data we want written
		synchronized (this.pendingData) {
			List queue = (List) this.pendingData.get(socket);
			if (queue == null) {
				queue = new ArrayList();
				this.pendingData.put(socket, queue);
			}
			queue.add(ByteBuffer.wrap(data));
		}

		// Finally, wake up our selecting thread so it can make the required changes
		this.selector.wakeup();
		//rspHandler.waitForResponse();
	}

	public void run() {
		while (t != null) {
			try {
				// Process any pending changes
				synchronized (this.pendingChanges) {
					Iterator changes = this.pendingChanges.iterator();
					while (changes.hasNext()) {
						ChangeRequest change = (ChangeRequest) changes.next();
						switch (change.type) {
						case ChangeRequest.CHANGEOPS:
							SelectionKey key = change.socket.keyFor(this.selector);
							key.interestOps(change.ops);
							break;
						case ChangeRequest.REGISTER:
							change.socket.register(this.selector, change.ops);
							break;
						}
					}
					this.pendingChanges.clear();
				}

				// Wait for an event one of the registered channels
				int keys = this.selector.select();
				if(keys == 0)
					continue;
				
				// Iterate over the set of keys for which events are available
				Iterator selectedKeys = this.selector.selectedKeys().iterator();
				while (selectedKeys.hasNext()) {
					SelectionKey key = (SelectionKey) selectedKeys.next();
					selectedKeys.remove();
					
					int readyOps = key.readyOps();
					
					if (!key.isValid()) {
						continue;
					}
					
					// Check what event is available and deal with it
					if (key.isConnectable()) {
						this.finishConnection(key);
					} else if (key.isReadable()) {
						this.read(key);
					} else if (key.isWritable()) {
						key.interestOps(key.interestOps() & ~readyOps);
						this.write(key);
					} 
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}

	private void read(SelectionKey key) throws IOException {
		SocketChannel socketChannel = (SocketChannel) key.channel();

		// Clear out our read buffer so it's ready for new data
		this.readBuffer.clear();

		// Attempt to read off the channel
		int numRead;
		try {
			numRead = socketChannel.read(this.readBuffer);
		} catch (IOException e) {
			// The remote forcibly closed the connection, cancel
			// the selection key and close the channel.
			key.cancel();
			return;
		}

		if (numRead == -1) {
			// Remote entity shut the socket down cleanly. Do the
			// same from our end and cancel the channel.
			key.channel().close();
			key.cancel();
			return;
		}

		// Handle the response
		this.handleResponse(socketChannel, this.readBuffer.array(), numRead);
	}

	private void handleResponse(SocketChannel socketChannel, byte[] data, int numRead) throws IOException {
		// Make a correctly sized copy of the data before handing it
		// to the client
		byte[] rspData = new byte[numRead];
		System.arraycopy(data, 0, rspData, 0, numRead);
		
		// Look up the handler for this channel
		RspHandler handler = (RspHandler) this.rspHandlers.get(socketChannel);
		
		// And pass the response to it
		if (handler.handleResponse(rspData)) {
			rspHandler.waitForResponse();
		}
	}

	private void write(SelectionKey key) throws IOException {
		SocketChannel socketChannel = (SocketChannel) key.channel();

		synchronized (this.pendingData) {
			List queue = (List) this.pendingData.get(socketChannel);

			// Write until there's not more data ...
			while (queue != null && !queue.isEmpty()) {
				ByteBuffer buf = (ByteBuffer) queue.get(0);
				socketChannel.write(buf);
				if (buf.remaining() > 0) {
					// ... or the socket's buffer fills up
					break;
				}
				queue.remove(0);
			}

			if (queue != null && queue.isEmpty()) {
				// We wrote away all data, so we're no longer interested
				// in writing on this socket. Switch back to waiting for
				// data.
				key.interestOps(SelectionKey.OP_READ);
			}
		}
	}

	private void finishConnection(SelectionKey key) throws IOException {
		SocketChannel socketChannel = (SocketChannel) key.channel();
	
		// Finish the connection. If the connection operation failed
		// this will raise an IOException.
		try {
			socketChannel.finishConnect();
		} catch (IOException e) {
			// Cancel the channel's registration with our selector
			System.out.println(e);
			key.cancel();
			return;
		}
	
		// Register an interest in writing on this channel
		key.interestOps(SelectionKey.OP_WRITE);
	}

	private SocketChannel initiateConnection() throws IOException {
		// Create a non-blocking socket channel
		SocketChannel socketChannel = SocketChannel.open();
		socketChannel.configureBlocking(false);
	
		// Kick off connection establishment
		socketChannel.connect(new InetSocketAddress(this.serverAddress, this.serverPort));
	
		// Queue a channel registration since the caller is not the 
		// selecting thread. As part of the registration we'll register
		// an interest in connection events. These are raised when a channel
		// is ready to complete connection establishment.
		synchronized(this.pendingChanges) {
			this.pendingChanges.add(new ChangeRequest(socketChannel, ChangeRequest.REGISTER, SelectionKey.OP_CONNECT));
		}
		//socketChannel.register(selector, socketChannel.validOps());
		return socketChannel;
	}

	private Selector initSelector() throws IOException {
		// Create a new selector
		return SelectorProvider.provider().openSelector();
	}
	
	public void stop() {
		t = null;
		try {
			socket.close();
		} catch(Exception e) { e.printStackTrace(); }
	}
	
	public void register(String username, String password) {
        try {
        	socket = this.initiateConnection();
    		t = new Thread(this, "Thread");
    		t.setDaemon(true);
    		t.start();
        } catch(Exception e) {
            e.printStackTrace();
            sipTalkUI.errorMessage(true, "Connection to Server Failed", "Error");
        }
        String message = packet.generateRegister(username, password);
        message = packet.appendHeader(message);
        try {
        	this.send(message.getBytes(), rspHandler);
        } catch (Exception e) { e.printStackTrace(); }
    }
	
	public void invite(String to) {
		if(socket != null) {
			if (this.uaState == this.REGISTERED) {
				this.bParty = to;
				String message = packet.generateInvite(to);
				message = packet.appendHeader(message);
				try {
					this.send(message.getBytes(), rspHandler);
					this.uaState = this.CALL_SETUP;
				} catch (Exception e) { e.printStackTrace(); }
			} else if (this.uaState == this.NOT_REGISTERED) {
				sipTalkUI.errorMessage(true, "User not logged in", "Error");
				sipTalkUI.loadPanel("login");
			} else if (this.uaState == this.CALL_SETUP || this.uaState == this.CALL_IN_PROGRESS) {
				sipTalkUI.errorMessage(false, "Call in progress", "Error");
			}
		}
	}
	
	public void cancel() {
		if(socket != null) {
			if (this.uaState == this.CALL_SETUP) {
				this.ringPlayer.closeLine();
				String message = packet.generateCancel(this.bParty);
				message = packet.appendHeader(message);
				try {
					this.send(message.getBytes(), rspHandler);
				} catch (Exception e) { e.printStackTrace(); }
				this.uaState = this.REGISTERED;
			} else if (this.uaState == this.CALL_IN_PROGRESS){
				String message = packet.generateBye(this.bParty);
				message = packet.appendHeader(message);
				try {
					this.send(message.getBytes(), rspHandler);
				} catch (Exception e) { e.printStackTrace(); }
				this.uaState = this.REGISTERED;
			}
		}
	}
	
	public void accept(String from) {
		if (socket != null) {
			String message = packet.generateAccept(this.findAndReplace(from));
			message = packet.appendHeader(message);
			try {
				this.send(message.getBytes(), rspHandler);
			} catch (Exception e) { e.printStackTrace(); }
			this.ringPlayer.closeLine();
			this.uaState = this.CALL_IN_PROGRESS;
		}
	}
	
	public void reject(String from) {
		if (socket != null) {
			String message = packet.generateReject(this.findAndReplace(from));
			message = packet.appendHeader(message);
			try {
				this.send(message.getBytes(), rspHandler);
			} catch (Exception e) { e.printStackTrace(); }
			this.ringPlayer.closeLine();
			this.uaState = this.REGISTERED;
		}
	}
	
	public String findAndReplace(String data) {
		data = replace(data, "&", "&amp;");
		data = replace(data, "<", "&lt;");
		data = replace(data, ">", "&gt;");
		data = replace(data, "'", "&apos;");
		data = replace(data, "\"", "&quot;");
		return data;
	}
	
	public String replace(String data, String patternStr, String replacementStr) {
		// Compile regular expression
		Pattern pattern = Pattern.compile(patternStr);
    
		// Replace all occurrences of pattern in input
		Matcher matcher = pattern.matcher(data);
		String output = matcher.replaceAll(replacementStr);
		return output;
	}
}
